using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;

namespace Comms.Drt {
    public class Server : IDisposable {
        public volatile bool IsDisposed;

        public Alarm Alarm;

        public int NextGameId;

        public byte[] DiscoveryResponseMessage;

        public double DiscoveryResponseMessageTime;

        public List<ServerGame> ServerGames = new();

        public MessageSerializer MessageSerializer;

        public int GameTypeID => MessageSerializer.GameTypeID;

        public float TickDuration { get; }

        public int StepsPerTick { get; }

        public Peer Peer { get; set; }

        public IPEndPoint Address => Peer.Address;

        public IReadOnlyList<ServerGame> Games => ServerGames;

        public ServerSettings Settings { get; } = new();

        public event Action<ResourceRequestData> ResourceRequest;

        public event Action<DesyncData> Desync;

        public event Action<Exception> Error;

        public event Action<string> Warning;

        public event Action<string> Information;

        public event Action<string> Debug;

        public Server(int gameTypeID, float tickDuration, int stepsPerTick, int localPort) : this(
            gameTypeID,
            tickDuration,
            stepsPerTick,
            new UdpTransmitter(localPort)
        ) { }

        public Server(int gameTypeID, float tickDuration, int stepsPerTick, ITransmitter transmitter) {
            if (tickDuration < 0.01f
                || tickDuration > 10f) {
                throw new ArgumentOutOfRangeException("tickDuration");
            }
            if (stepsPerTick < 1) {
                throw new ArgumentOutOfRangeException("stepsPerTick");
            }
            TickDuration = tickDuration;
            StepsPerTick = stepsPerTick;
            MessageSerializer = new MessageSerializer(gameTypeID);
            Peer = new Peer(transmitter);
            Peer.Settings.SendPeerConnectDisconnectNotifications = false;
            Peer.Error += delegate(Exception e) {
                if (e is MalformedMessageException ex) {
                    InvokeWarning($"Malformed message from {ex.SenderAddress} ignored. {ex.Message}");
                }
                else {
                    InvokeError(e);
                }
            };
            Peer.PeerDiscoveryRequest += delegate(Packet p) {
                if (!IsDisposed) {
                    Message message = MessageSerializer.Read(p.Bytes, p.Address);
                    if (!(message is ClientDiscoveryRequestMessage message2)) {
                        if (!(message is ClientResourceRequestMessage message3)) {
                            throw new ProtocolViolationException($"Unexpected message type {message.GetType()}.");
                        }
                        Handle(message3, p.Address);
                    }
                    else {
                        Handle(message2, p.Address);
                    }
                }
            };
            Peer.ConnectRequest += delegate(PeerPacket p) {
                if (!IsDisposed) {
                    Message message4 = MessageSerializer.Read(p.Bytes, p.PeerData.Address);
                    if (!(message4 is ClientCreateGameRequestMessage message5)) {
                        if (!(message4 is ClientJoinGameRequestMessage message6)) {
                            throw new ProtocolViolationException($"Unexpected message type {message4.GetType()}.");
                        }
                        Handle(message6, p.PeerData);
                    }
                    else {
                        Handle(message5, p.PeerData);
                    }
                }
            };
            Peer.DataMessageReceived += delegate(PeerPacket p) {
                if (!IsDisposed) {
                    Message message7 = MessageSerializer.Read(p.Bytes, p.PeerData.Address);
                    if (!(message7 is ClientJoinGameAcceptedMessage message8)) {
                        if (!(message7 is ClientJoinGameRefusedMessage message9)) {
                            if (!(message7 is ClientInputMessage message10)) {
                                if (!(message7 is ClientStateMessage message11)) {
                                    if (!(message7 is ClientDesyncStateMessage message12)) {
                                        if (!(message7 is ClientStateHashesMessage message13)) {
                                            if (!(message7 is ClientGameDescriptionMessage message14)) {
                                                throw new ProtocolViolationException($"Unexpected message type {message7.GetType()}.");
                                            }
                                            Handle(message14, p.PeerData);
                                        }
                                        else {
                                            Handle(message13, p.PeerData);
                                        }
                                    }
                                    else {
                                        Handle(message12, p.PeerData);
                                    }
                                }
                                else {
                                    Handle(message11, p.PeerData);
                                }
                            }
                            else {
                                Handle(message10, p.PeerData);
                            }
                        }
                        else {
                            Handle(message9, p.PeerData);
                        }
                    }
                    else {
                        Handle(message8, p.PeerData);
                    }
                }
            };
            Peer.PeerDisconnected += delegate(PeerData p) {
                if (!IsDisposed) {
                    HandleDisconnect(p);
                }
            };
        }

        public void Dispose() {
            lock (Peer.Lock) {
                if (IsDisposed) {
                    return;
                }
                IsDisposed = true;
            }
            Alarm?.Dispose();
            Peer?.Dispose();
        }

        public void Start() {
            lock (Peer.Lock) {
                CheckNotDisposed();
                if (Alarm != null) {
                    throw new InvalidOperationException("Server is already started.");
                }
                InvokeInformation($"Server {Address} started at {DateTime.UtcNow}");
                Peer.Start();
                Alarm = new Alarm(AlarmFunction);
                Alarm.Error += delegate(Exception e) { InvokeError(e); };
                Alarm.Set(0.0);
            }
        }

        public void DisconnectAllClients() {
            CheckNotDisposedAndStarted();
            Peer.DisconnectAllPeers();
        }

        public void SendResource(IPEndPoint address, string name, int version, byte[] bytes) {
            Peer.RespondToDiscovery(
                address,
                DeliveryMode.Reliable,
                MessageSerializer.Write(new ServerResourceMessage { Name = name, Version = version, Bytes = bytes })
            );
        }

        public void AlarmFunction() {
            lock (Peer.Lock) {
                if (IsDisposed) {
                    return;
                }
                double time = Comm.GetTime();
                double num = double.PositiveInfinity;
                foreach (ServerGame serverGame in ServerGames) {
                    double val = serverGame.Run(time);
                    num = Math.Min(num, val);
                }
                ServerGames.RemoveAll(
                    delegate(ServerGame g) {
                        if (g.Clients.Count == 0) {
                            InvokeInformation($"Game {g.GameID} finished.");
                            return true;
                        }
                        return false;
                    }
                );
                Alarm.Set(Math.Max(num - Comm.GetTime(), 0.0));
            }
        }

        public void CheckNotDisposed() {
            if (IsDisposed) {
                throw new ObjectDisposedException("Server");
            }
        }

        public void CheckNotDisposedAndStarted() {
            CheckNotDisposed();
            if (Alarm == null) {
                throw new InvalidOperationException("Server is not started.");
            }
        }

        public void Handle(ClientDiscoveryRequestMessage message, IPEndPoint address) {
            double time = Comm.GetTime();
            if (DiscoveryResponseMessage == null
                || time > DiscoveryResponseMessageTime + Settings.GameListCacheTime) {
                DiscoveryResponseMessageTime = time;
                DiscoveryResponseMessage = MessageSerializer.Write(
                    new ServerDiscoveryResponseMessage {
                        Name = Settings.Name,
                        Priority = Settings.Priority,
                        GamesDescriptions =
                            (from g in ServerGames.OrderBy(g => g.Tick).Take(Settings.MaxGamesToList) select g.CreateGameDescription()).ToArray()
                    }
                );
            }
            Peer.RespondToDiscovery(address, DeliveryMode.Unreliable, DiscoveryResponseMessage);
        }

        public void Handle(ClientResourceRequestMessage message, IPEndPoint address) {
            ResourceRequest?.Invoke(new ResourceRequestData { Address = address, Name = message.Name, MinimumVersion = message.MinimumVersion });
        }

        public void Handle(ClientCreateGameRequestMessage message, PeerData peerData) {
            if (Settings.Priority <= 0) {
                Peer.RefuseConnect(
                    peerData,
                    MessageSerializer.Write(new ServerConnectRefusedMessage { Reason = "Server is currently not accepting new games." })
                );
                InvokeWarning($"Create game request from client \"{message.ClientName}\" at {peerData.Address} refused because priority is <= 0.");
                return;
            }
            if (ServerGames.Count >= Settings.MaxGames) {
                Peer.RefuseConnect(peerData, MessageSerializer.Write(new ServerConnectRefusedMessage { Reason = "Too many games." }));
                InvokeWarning(
                    $"Create game request from client \"{message.ClientName}\" at {peerData.Address} refused because of too many games ({ServerGames.Count})."
                );
                return;
            }
            ServerGame serverGame = new(this, peerData, NextGameId++, message);
            ServerGames.Add(serverGame);
            InvokeInformation($"Client \"{message.ClientName}\" at {peerData.Address} created game {serverGame.GameID}.");
            Peer.AcceptConnect(
                peerData,
                MessageSerializer.Write(
                    new ServerCreateGameAcceptedMessage {
                        GameID = serverGame.GameID,
                        CreatorAddress = peerData.Address,
                        TickDuration = TickDuration,
                        StepsPerTick = StepsPerTick,
                        DesyncDetectionMode = serverGame.DesyncDetectionMode,
                        DesyncDetectionPeriod = serverGame.DesyncDetectionPeriod
                    }
                )
            );
            Alarm.Set(0.0);
        }

        public void Handle(ClientJoinGameRequestMessage message, PeerData peerData) {
            ServerGame serverGame = ServerGames.FirstOrDefault(g => g.GameID == message.GameID);
            if (serverGame != null) {
                serverGame.Handle(message, peerData);
                return;
            }
            InvokeWarning($"Join game request from {peerData.Address} for nonexistent game {message.GameID}.");
            Peer.RefuseConnect(peerData, MessageSerializer.Write(new ServerConnectRefusedMessage { Reason = "Game does not exist." }));
        }

        public void Handle(ClientJoinGameAcceptedMessage message, PeerData peerData) {
            ServerClient serverClient = ServerClient.FromPeerData(peerData);
            if (serverClient != null) {
                serverClient.ServerGame.Handle(message, serverClient);
            }
            else {
                InvokeWarning($"Game join accepted from {peerData.Address}, which is not a connected client.");
            }
        }

        public void Handle(ClientJoinGameRefusedMessage message, PeerData peerData) {
            ServerClient serverClient = ServerClient.FromPeerData(peerData);
            if (serverClient != null) {
                serverClient.ServerGame.Handle(message, serverClient);
            }
            else {
                InvokeWarning($"Game join refused from {peerData.Address}, which is not a connected client.");
            }
        }

        public void Handle(ClientInputMessage message, PeerData peerData) {
            ServerClient serverClient = ServerClient.FromPeerData(peerData);
            if (serverClient != null) {
                serverClient.ServerGame.Handle(message, serverClient);
            }
            else {
                InvokeWarning($"Input from {peerData.Address}, which is not a connected client.");
            }
        }

        public void Handle(ClientStateMessage message, PeerData peerData) {
            ServerClient serverClient = ServerClient.FromPeerData(peerData);
            if (serverClient != null) {
                serverClient.ServerGame.Handle(message, serverClient);
            }
            else {
                InvokeWarning($"State from {peerData.Address}, which is not a connected client.");
            }
        }

        public void Handle(ClientDesyncStateMessage message, PeerData peerData) {
            ServerClient serverClient = ServerClient.FromPeerData(peerData);
            if (serverClient != null) {
                serverClient.ServerGame.Handle(message, serverClient);
            }
            else {
                InvokeWarning($"Desync state from {peerData.Address}, which is not a connected client.");
            }
        }

        public void Handle(ClientStateHashesMessage message, PeerData peerData) {
            ServerClient serverClient = ServerClient.FromPeerData(peerData);
            if (serverClient != null) {
                serverClient.ServerGame.Handle(message, serverClient);
            }
            else {
                InvokeWarning($"State hashes from {peerData.Address}, which is not a connected client.");
            }
        }

        public void Handle(ClientGameDescriptionMessage message, PeerData peerData) {
            ServerClient serverClient = ServerClient.FromPeerData(peerData);
            if (serverClient != null) {
                serverClient.ServerGame.Handle(message, serverClient);
            }
            else {
                InvokeWarning($"Game description from {peerData.Address}, which is not a connected client.");
            }
        }

        public void HandleDisconnect(PeerData peerData) {
            ServerClient serverClient = ServerClient.FromPeerData(peerData);
            if (serverClient != null) {
                serverClient.ServerGame.HandleDisconnect(serverClient);
            }
            else {
                InvokeWarning($"Disconnect received from {peerData.Address}, which is not a connected client.");
            }
        }

        public void InvokeDesync(DesyncData desyncData) {
            Desync?.Invoke(desyncData);
        }

        public void InvokeError(Exception error) {
            Error?.Invoke(error);
        }

        public void InvokeWarning(string warning) {
            Warning?.Invoke(warning);
        }

        public void InvokeInformation(string information) {
            Information?.Invoke(information);
        }

        [Conditional("DEBUG")]
        public void InvokeDebug(string format, params object[] args) {
            if (Debug != null) {
                Debug?.Invoke(string.Format(format, args));
            }
        }
    }
}